package com.hup.minicontroller.common.bluetooth;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;

import com.hup.minicontroller.common.bluetooth.framework.BtChannel;
import com.hup.minicontroller.common.bluetooth.framework.BtChannelHandler;
import com.hup.minicontroller.common.bluetooth.handler.BtDataHandler;
import com.hup.minicontroller.common.bluetooth.handler.BtDataHandler.DataMethodInvoker;
import com.hup.minicontroller.common.bluetooth.handler.BtDataHandlerRegister;
import com.hup.minicontroller.common.bluetooth.handler.BtHeartbeatHandler.BtIdleCallback;
import com.hup.minicontroller.common.bluetooth.handler.BtLoginResponseHandler;
import com.hup.minicontroller.common.bluetooth.handler.BtLoginResponseHandler.BtLoginResponseCallback;
import com.hup.minicontroller.common.bluetooth.model.BaseBtDataPacket;
import com.hup.minicontroller.common.bluetooth.model.BtLoginResponsePacket;
import com.hup.minicontroller.common.thread.component.EfficientExecutor;
import com.hup.minicontroller.common.util.Supplier;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import lombok.Getter;
import lombok.Setter;

/**
 * BluetoothSocket客户端;<pre>
 * 客户端的线程池:
 * .只在 {@link #BluetoothClient(BtClientConfig)}构造时进行初始化,然后就不能修改了
 * .在{@link #doTerminate()}时才会释放,因为相当于服务端,客户端会比更频繁的连接和断开,期间不需要修改线程池策略
 * .{@link .#doTerminate()} 销毁后,本实例就不能重复使用! 因为 {@link ConnectedObj#releaseExecutor()}
 *
 * @author hugan
 * @date 2021/5/14
 */
public class BluetoothClient {
    private static final String TAG = BluetoothClient.class.getSimpleName();

    private final BtClientConfig clientConfig;
    private final ConnectedObj connectedObj;
    private final MyClientHandler clientHandlerInstance;
    private final BtLoginResponseHandler loginHandlerInstance;

    public BluetoothClient(BtClientConfig clientConfig) {
        this.clientConfig = clientConfig;
        connectedObj = new ConnectedObj();
        connectedObj.buildExecutor(clientConfig);
        connectedObj.dataHandlerMap = clientConfig.registerDataHandlerMap();
        clientHandlerInstance = new MyClientHandler();
        if (clientConfig.loginResponseCallback != null) {
            loginHandlerInstance = new BtLoginResponseHandler(clientConfig.loginResponseCallback);
        } else {
            loginHandlerInstance = null;
        }
    }

    /**
     * 连接服务端;
     * 注意:连接前
     * .要先停止蓝牙扫描 {@link BluetoothSocket#connect()}
     * .传入目标服务端蓝牙设备 {@link BtClientConfig#setTargetServer(BluetoothDevice)}
     */
    public void connect() {
        connectedObj.assertState(ConnectState.DISCONNECT);
        Log.i(TAG, "connect: 正在连接服务端");
        connectedObj.connectState = ConnectState.CONNECTING;

        connectedObj.executor.execute(() -> {
            try {
                BluetoothSocket clientSocket = clientConfig.targetServer.createRfcommSocketToServiceRecord(clientConfig.uuid);
                clientSocket.connect();//阻塞线程,直到方法完成或异常

                connectedObj.btChannel = BtChannel.clientChannel(clientSocket, clientHandlerInstance,
                        connectedObj.dataHandlerMap,
                        loginHandlerInstance,
                        clientConfig.idleCallback
                );
            } catch (Exception e) {
                clientConfig.clientCallback.clientExceptionCaught(e);
                connectedObj.connectState = ConnectState.DISCONNECT;
                clientConfig.clientCallback.onConnectResult(false, BtLoginResponsePacket.failure("连接失败"));
                return;
            }
            /*
            直接在当前线程中处理client:初始化,接收数据等
            connectState至此一直是[连接中],在认证通过或失败后,才由btChannel通知clientCallback进行更新
             */
            connectedObj.btChannel.run();
        });
    }

    public void disconnect() {
        connectedObj.assertState(ConnectState.CONNECTED);
        Log.i(TAG, "正在断开连接");
        connectedObj.btChannel.disconnect();
    }

    public ConnectState getConnectState() {
        return connectedObj.connectState;
    }

    public void send(BaseBtDataPacket data) {
        connectedObj.assertState(ConnectState.CONNECTED);
        connectedObj.btChannel.send(data);
    }

    public void doTerminate() {
        if (ConnectState.CONNECTED.equals(connectedObj.connectState)) {
            disconnect();
            connectedObj.releaseExecutor();
        } else {
            connectedObj.releaseExecutor();
        }
    }

    private class MyClientHandler implements BtChannelHandler {

        @Override
        public void onConnectResult(boolean success, BtChannel channel) {
            connectedObj.connectState = success ? ConnectState.CONNECTED : ConnectState.DISCONNECT;
            clientConfig.clientCallback.onConnectResult(success, channel.getLoginResponse());
        }

        @Override
        public void onChannelClosed(BtChannel channel) {
            connectedObj.connectState = ConnectState.DISCONNECT;
            clientConfig.clientCallback.onConnectionLost();
        }

        @Override
        public void onUnknownData(BtChannel channel, BaseBtDataPacket data) {
            clientConfig.clientCallback.onUnknownData(data);
        }

        @Override
        public void exceptionCaught(BtChannel channel, Throwable e) {
            clientConfig.clientCallback.clientExceptionCaught(e);
        }
    }

    /**
     * 客户端连接后的状态对象
     */
    private static class ConnectedObj {

        private ConnectState connectState = ConnectState.DISCONNECT;
        private ThreadPoolExecutor executor;
        private Map<Class<? extends BaseBtDataPacket>, DataMethodInvoker> dataHandlerMap;
        private BtChannel btChannel;

        private void assertState(ConnectState state) {
            if (connectState != state) throw new IllegalArgumentException("连接状态为:" + connectState.desc);
        }

        private void buildExecutor(BtClientConfig clientConfig) {
            executor = clientConfig.executorSupplier.get();
        }

        private void releaseExecutor() {
            executor.shutdown();
            executor = null;
        }
    }

    /**
     * 客户端连接配置类;
     * 配置在下一次 {@link BluetoothClient#connect()}时生效
     */
    public static class BtClientConfig implements BtDataHandlerRegister {

        private final UUID uuid;
        private final BtClientCallback clientCallback;
        @Setter
        private BluetoothDevice targetServer;
        /**
         * 登陆认证处理回调,null-没登陆认证功能
         */
        @Setter
        private BtLoginResponseCallback loginResponseCallback;
        /**
         * 心跳功能的回调,null-无心跳功能
         */
        @Setter
        private BtIdleCallback idleCallback;

        @Getter
        private ArrayList<BtDataHandler> lstDataHandler = new ArrayList<>();
        /**
         * 线程池策略
         */
        @Setter
        private Supplier<ThreadPoolExecutor> executorSupplier = () -> new EfficientExecutor(
                1,//核心线程=客户端线程1
                1,//最大线程=核心线程数
                0,//没有等待队列,满员就抛RejectedExecutionException
                30, TimeUnit.SECONDS,
                "BtClient-",
                "蓝牙客户端");

        public BtClientConfig(UUID uuid, BtClientCallback clientCallback) {
            this.uuid = uuid;
            this.clientCallback = clientCallback;
        }
    }

    /**
     * 客户端启动状态
     */
    public enum ConnectState {
        DISCONNECT("已断开"),
        CONNECTED("已连接"),
        CONNECTING("连接中");
        private final String desc;

        ConnectState(String desc) {
            this.desc = desc;
        }
    }

    /**
     * 客户端状态回调
     */
    public interface BtClientCallback {

        /**
         * 连接服务端结果回调
         *
         * @param loginResponse 登陆认证回复,有认证功能时才不为null,数据类型是绑定 {@link BtLoginResponsePacket}时配置的
         */
        void onConnectResult(boolean success, BtLoginResponsePacket loginResponse);

        /**
         * 连接已断开
         */
        void onConnectionLost();

        /**
         * 收到客户端的未知消息
         */
        default void onUnknownData(BaseBtDataPacket data) {
            Log.w(TAG, "onUnknownData: 未知数据类型=" + data);
        }

        default void clientExceptionCaught(Throwable e) {
            if (e instanceof IOException) {
                /*
                代码没bug时,一般是server程序非正常结束,没有正常关闭socket
                    导致IO异常:远程主机强迫关闭了一个现有的连接
                    此时onConnectionLost已经通知callback了,所以不需要打印
                注意:如果是其他问题,如代码bug引起的IO异常,则此处是一个大坑,会屏蔽异常信息,
                    但代码规范是,实现业务逻辑时,应自己捕获IO异常!
                 */
                return;
            }
            Log.e(TAG, "clientExceptionCaught: ", e);
        }
    }

}
